Hit-Testing Layout Shapes
This programming recipe provides feedback when the user presses the
mouse button on a layout shape. If the user simply clicks on the layout shape, indicating a caret selection, the code in this recipe draws a caret, as shown
in Figure 7-1.Figure 7-1 Sample caret selection
If the user drags the mouse throughout the layout shape, the code in this recipe provides selection feedback, tracking the mouse and allowing the user to create range selections, as shown in Figure 7-2.
Figure 7-2 Sample range selection
This recipe makes extensive use of the QuickDraw GX selection library and the QuickDraw GX layout edit library. These libraries contain data types and functions that help you provide selection feedback for your application's users. In fact, all of the functionality described in this recipe--and much more--is implemented for you in those libraries.
Here are a few key concepts you need to understand before beginning
this recipe:
- An offset is an index into the text of a layout, specifying a position between characters. For example, an offset of 0 represents the position before the first character of layout text and an offset of 1 represents the position between the first and second characters.
- A selection is a range of text in a layout. A selection is typically specified using two offsets: the beginning of the selection and the end of the selection.
- The highlight shape is a shape that represents the area that needs to be highlighted to show the user which characters are selected. The highlight shape can describe a caret or a range selection. For example, if the first three characters of a layout are selected, the highlight shape might be the rectangle that encloses the first three glyphs of the layout shape as drawn. If the selection is a caret selection, the highlight shape represents the actual caret as drawn.
- Note
- The examples given above describe a simple selection and highlight shape--you can use the layout shape and the layout edit library to provide sophisticated features, such as multiple-range selections, discontiguous highlighting, and slanted highlighting.<8bat>u
![]()
Overview of Recipe Steps
The steps in this recipe show you how to:
You need to follow the instructions in each of these steps to verify a hit-test on a layout shape and to provide layout selection feedback.
- Determine if a layout shape was hit
- Create a layout edit structure
- Hit-test the layout shape
- Erase any previous highlight
- Make a new selection and highlight
- Provide selection feedback while the user drags the mouse
- Determine if the mouse has moved to a new offset
- Determine if the selection has changed from a caret to a highlight
- Redraw the caret or highlight
- Draw the difference between the old and new highlights
Functions Used in This Recipe
QuickDraw GX functions used in this recipe:
GXHitTestPicture
"Picture Shapes"
QuickDraw GX GraphicsGXGetShapeType
"Shape Objects"
QuickDraw GX ObjectsGXHitTestLayout
"Layout Carets, Highlighting,
and Hit-Testing"
QuickDraw GX TypographyGXDrawShape
"Shape Objects"
QuickDraw GX ObjectsGXGetShapeViewPorts
"Transform Objects"
QuickDraw GX ObjectsGXGetViewPortMouse
"QuickDraw GX and the
Macintosh Environment"
QuickDraw GX Environment and UtilitiesGXCopyToShape
"Shape Objects"
QuickDraw GX ObjectsGXExcludeShape
"Geometric Operations"
QuickDraw GX GraphicsGXDisposeShape
"Shape Objects"
QuickDraw GX ObjectsThis recipe gives a brief description of these functions; you can find complete reference information for them in the Inside Macintosh suite of books.
This recipe also uses functions from the QuickDraw GX libraries:
LayoutEditHandleFromLayout
layout edit library LockEditHandle
layout edit library NewSelectionAndHighlight
layout edit library Recipe Step Descriptions
In this section, each step is described individually.
- Determine if a layout shape was hit
Before you begin providing selection feedback, you need to determine if
the user hit a layout shape and, if so, which layout shape was hit. If your layout shapes are stored in a picture with other shapes, you can use theGXHitTestPicture
function to determine which shape was hit, as you
did when you hit-tested graphics shapes in the last chapter. As you did in that chapter, you must first convert the hit point from QuickDraw global coordinates to QuickDraw GX local coordinates and store the resulting QuickDraw GX point structure in a variable namedoriginalPoint
. Then you can callGXHitTestPicture
to hit-test the picture:
hitShape = GXHitTestPicture(gCurrent->picture,
originalPoint,
&result, 1, 1);You can determine whether the hit shape was a layout shape by examining its type using the
GXGetShapeType
function. If the expression
GXGetShapeType(hitShape) == gxLayoutTypereturns
true
, a layout shape was hit, and you can proceed to Step 2.- Create a layout edit structure
The layout edit library provides a layout edit structure for you to use to store information about a layout shape's current selection and the current high-
light shape.This step shows you how to create a layout edit structure. Later in this recipe, you use these fields of the structure:
- The
layout
field contains a reference to the layout shape containing
the selection.- The
highlight
field contains a reference to the highlight shape.You use the
LayoutEditHandle
type, provided in the layout edit library, to declare a handle to a layout edit structure:
LayoutEditHandle myLayoutEditHandle;When the user presses the mouse button on a layout shape, you could use this
if
statement to create a layout edit structure:
if (MyLayoutAlreadyHasSelection(hitShape))
myLayoutEditHandle = MyLookupLayoutEdit(hitShape);
else
myLayoutEditHandle = LayoutEditHandleFromLayout(hitShape);In your
MyLayoutAlreadyHasSelection
function, you could determine
if you've already created a layout edit structure for the layout shape that was hit and in yourMyLookupLayoutEdit
function you could return the previously created layout edit handle;As a simple example, if your application allows selection in a single layout shape at a time, you could store a reference to the currently selected layout shape and the handle to its layout edit structure in global variables. You could then implement your
MyLayoutAlreadyHasSelection
function with this line of code:
return (hitShape == gCurrentlySelectedLayout);and your
MyLookupLayoutEdit
function with this line of code:
return gCurrentLayoutEditHandle;If you determine that the layout shape that the user hit does not
already have a layout edit structure, you call the library functionLayoutEditHandleFromLayout
to create one.While the user is dragging the mouse and you're providing selection feed-
back, you should lock the layout edit structure in memory and use a pointer to reference it. You can use the library functionLockEditHandle
to lock
the layout edit structure in memory. This function returns a pointer to
the structure:
LayoutEditPtr myLayoutEdit;
myLayoutEdit = LockEditHandle(myLayoutEditHandle);Most of the layout edit library functions use layout edit pointers rather than layout edit handles.
- Note
- The
LayoutEditPtr
data type is one of the data types used internally by the layout edit library because normally you don't have to know the internal structure of the layout edit structure to use the layout edit library. However, the purpose of this recipe is to introduce
how the layout edit library implements some of its commonly used functions. To follow the steps of this recipe, you need to include the definition of the layout edit structure in your application, which you can do simply by including thelayout edit library.c
file.<8bat>u![]()
- Hit-test the layout shape
Now that you have prepared your layout edit structure, you're ready to hit-test the layout. You use the
GXHitTestLayout
function, which returns information in a layout hit-test information structure, as defined by thegxLayoutHitInfo
data type. Before you callGXHitTestLayout
, you need
to declare such a structure to store the results:
gxLayoutHitInfo hitInfo;Then you can call
GXHitTestLayout
to determine information about
the hit-test:
GXHitTestLayout(myLayoutEdit->layout, &originalPoint,
gxHighlightAverageAngle, &hitInfo, nil);This function examines the hit point and analyzes the layout shape to determine the hit offset--that is, where in the text the user pressed on the mouse. As a simple example, if the text of the layout is "Layout Shape" and the user clicked the mouse between the "y" glyph and the "o" glyph, this function calculates that the hit offset into the layout text is 3--indicating the layout was hit between the third and fourth characters in the text.
The
GXHitTestLayout
function returns the hit offset in thehitSideOffset
field of the layout hit-test information structure. The selection library provides theSelectionOffset
type, which you can use to store the offset:
SelectionOffset originalOffset;
originalOffset = (SelectionOffset) hitInfo.hitSideOffset;You use this offset when providing selection feedback in Step 6.
- Erase any previous highlight
If your layout shape already had a highlighted selection, you need to dehighlight the old selection by erasing the highlight shape stored in the
highlight
field of the layout edit structure. The layout edit library uses a transfer mode for highlight shapes that is similar to the exclusive-OR mode you used in the last chapter to provide feedback when the user dragged the mouse. As a result, you can dehighlight the previous selection simply by calling drawing the highlight shape, thus:
if (myLayoutEdit->highlight != nil)
GXDrawShape(myLayoutEdit->highlight);- Make a new selection and highlight
When the user presses the mouse button on your layout shape, the selection and highlight shape both change, so you need to update the information in your layout edit structure.
The library function
NewSelectionAndHighlight
stores the new selection and uses it to update the highlight shape. To do this, you can call:
NewSelectionAndHighlight(myLayoutEdit,
originalOffset,
originalOffset);This function takes three parameters:
- The first parameter specifies the layout edit structure.
- The second parameter specifies the beginning offset of the selection.
- The third parameter specifies the final offset of the selection.
Since you specify the hit offset as both the beginning offset and the final offset, this function creates a caret selection and a caret highlight. You can draw the caret by drawing the highlight shape:
GXDrawShape(myLayoutEdit->highlight);
- Provide selection feedback while the user drags the mouse
To provide selection feedback while the user drags the mouse, you
can repeatedly call theGXGetViewPortMouse
function to find the current mouse position, and then redraw the selection accordingly. To use theGXGetViewPortMouse
function, you need a reference to the view port. You can store a reference to the view port in a global variable, in your document information structure, or you can obtain it directly from your layout shape:
gxViewPort layoutViewPort;
GXGetShapeViewPorts(myLayoutEdit->layout, &layoutViewPort);Here is the flow of control for the selection feedback loop:
gxPoint oldPoint, newPoint;
SelectionOffset oldOffset, newOffset;
oldPoint.x = originalPoint.x;
oldPoint.y = originalPoint.y;
oldOffset = originalOffset;
while (Button()) {
GXGetViewPortMouse(layoutViewPort, &newPoint);
if (MouseMoved(newPoint, oldPoint)) {
/* Draw new selection as appropriate. See Steps 7-10. */
}
}The following steps--Steps 7 through 10--show how to give feedback if the mouse has moved since the last time through the loop.
- Determine if the mouse has moved to a new offset
Even after you've determined that the mouse has moved, you still need to determine whether it moved enough to change the selection. You can use the
GXHitTestLayout
function to determine the new offset implied by the mouse location:
GXHitTestLayout(myLayoutEdit->layout, &newPoint,
gxHighlightAverageAngle, &hitInfo, nil);
newOffset = hitInfo.hitSideOffset;and then compare the new offset with the offset from the previous time through the loop:
if (newOffset != oldOffset) {
/* Highlight new selection. See Steps 8-10. */
}The following steps, Steps 8 through 10, show how to change the selection to reflect the new mouse position.
- Determine if the selection has changed from a caret to a highlight
To change the highlighted selection, you could simply erase the old high-
light shape, calculate the new highlight shape, and draw the new highlight shape. In fact, this is exactly what you do if the selection is changing from
a caret selection to a range selection, or from a range selection to a
caret selection.However, if the selection is changing from one range selection to another, the old range and the new range probably overlap. If you erase the entire old highlight and draw the new highlight shape, the user will see a flicker when dragging the mouse, so you need to handle this case differently.
First, however, you have to determine if the selection is changing from a caret to a range or vice versa. You can use these variables to save informa-
tion about the type of selection:
boolean oldWasCaret = true,
newIsCaret = true;Then, to test if the selection type is changing, you can use this code:
newIsCaret = (newOffset == originalOffset);
if (oldWasCaret != newIsCaret) {
/* Redraw entire highlight. See Step 9. */
} else {
/* Draw the difference between highlights. See Step 10. */
}The next two steps show how to update the highlight for these two cases.
- Redraw the caret or highlight
To redraw the entire highlight shape, you simply draw the old shape to erase it, calculate the new highlight shape using the library function
NewSelectionAndHighlight
, and then draw the new highlight shape.
Here is the code:
GXDrawShape(myLayoutEdit->highlight); /* erase old */
NewSelectionAndHightlight(myLayoutEdit,
originalOffset,
newOffset);
GXDrawShape(myLayoutEdit->highlight); /* draw new */- Draw the difference between the old and the new highlight
If the selection is changing from one range to another, you only need to draw the difference between the old highlight shape and the new highlight shape. You can use the QuickDraw GX function
GXExcludeShape
to find the difference between the two highlight shapes.You'll need to store references to the old highlight shape and to a shape representing the differences between the two highlight shapes:
gxShape oldHighlight = nil,
differenceHighlight = nil;Here is the code:
oldHighlight = GXCopyToShape(oldHighlight,
myLayoutEdit->highlight);
NewSelectionAndHighlight(myLayoutEdit,
originalOffset,
newOffset);
differenceHighlight = GXCopyToShape(differenceHighlight,
myLayoutEdit->highlight);
GXExcludeShape(differenceHighlight, oldHighlight);
GXDrawShape(differenceHighlight);When you finish using the two highlight shapes, dispose of them:
GXDisposeShape(differenceHighlight);
GXDisposeShape(oldHighlight);Finally, after you've updated the highlight, set up the variables for the next time through the loop:
oldPoint.x = newPoint.x;
oldPoint.y = newPoint.y;
oldOffset = newOffset;
oldWasCaret = newIsCaret;
Related Recipes
The next recipe, "Text Editing," shows how you can allow the user to edit text in a layout shape. The code in that recipe responds to key presses by deleting the selected text from a layout and inserting the specified character.The recipes in Chapter 4, "Using the QuickDraw GX Environment," show you how to initialize QuickDraw GX. You should read the recipes in that chapter before using any recipes in this chapter.
The recipes in Chapter 5, "Using Macintosh Windows," show you how to
use QuickDraw GX with Macintosh windows. You need to be familiar with
the information in that chapter before you can display layout shapes in a Macintosh window.